Skip to content

Conversation

@nbarbettini
Copy link
Contributor

@nbarbettini nbarbettini commented Nov 13, 2025

Motivation and Context

Closes #919

Implements SEP-1036, coming in the next version of MCP.

How Has This Been Tested?

  • New unit tests covering business logic
  • New client+server example added

Breaking Changes

Yes (only for existing elicitation implementations):

  • client.setRequestHandler(ElicitRequestSchema, () => ...) may need to add an additional check on the params.mode parameter to keep TS happy:
client.setRequestHandler(ElicitRequestSchema, async request => {
    if (request.params.mode !== 'form') {
        throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
    }
    // ... rest of the handler

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

I organized the PR by commit, so hopefully it is easy to follow!
I will add some review notes inline in the code below.

@nbarbettini nbarbettini requested review from a team as code owners November 13, 2025 00:56
@nbarbettini
Copy link
Contributor Author

nbarbettini commented Nov 13, 2025

Current status:

✅ Spec type checks and tests are passing

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 13, 2025

Open in StackBlitz

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/sdk@1105

commit: 7de4a7b

Comment on lines +236 to +238
if (request.params.mode !== 'form') {
throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is technically redundant, since the base client already validates the mode before invoking this handler. However, the request handler is typed to ElicitRequestSchema because there is not an ElicitFormRequestSchema (there is an ElicitRequestFormParamsSchema, but that is an inner shape). So without this code checking for mode="form", TypeScript complains because it is not 100% sure if the payload is of the right type.

I didn't want to boil the ocean and change too much of the contract of client.setRequestHandler(ElicitRequestSchema...), but please give me feedback if you think I should change this further!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine, we could technically achieve setting a request handler by having 2 more schemas ElicitRequestURLSchema, ElicitRequestFormSchema, but I think that locks us down further by introducing new public interfaces.

While the client does check this under the hood out of the box, I think it doesn't hurt too much for the end user to be aware that there are multiple modes and to be having to write some checks/guardrails their side about the mode and generally have awareness of how they handle each mode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good w/ me!

@nbarbettini nbarbettini force-pushed the feat/url-elicitation-final branch from 0593256 to 4b4516f Compare November 13, 2025 22:27
@nbarbettini
Copy link
Contributor Author

nbarbettini commented Nov 13, 2025

Now the only remaining breaking change can be seen in: https://github.com/modelcontextprotocol/typescript-sdk/pull/1105/files/237f84c6b168cbc74a4bc3836846a915654df390#diff-629b7a15c09a4a699f9f49eac4d4768c03b1c42e0856c687c827ed000e9171eeR235-R238

Previously a client using the SDK could do

client.setRequestHandler(ElicitRequestSchema, async request => {
    console.log('\n🔔 Elicitation Request Received:');
    // ...

But now because ElicitRequestSchema.params = z.union([ElicitRequestFormParamsSchema, ElicitRequestURLParamsSchema]) I need to do:

client.setRequestHandler(ElicitRequestSchema, async request => {
    if (request.params.mode !== 'form') {
        throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
    }
    console.log('\n🔔 Elicitation (form) Request Received:');
    // ...

otherwise I get: Property 'requestedSchema' does not exist on type...

@KKonstantinov Do you want me to try some additional shenanigans to make existing code that calls setRequestHandler(ElicitRequestSchema...) not break?

Comment on lines +236 to +238
if (request.params.mode !== 'form') {
throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine, we could technically achieve setting a request handler by having 2 more schemas ElicitRequestURLSchema, ElicitRequestFormSchema, but I think that locks us down further by introducing new public interfaces.

While the client does check this under the hood out of the box, I think it doesn't hurt too much for the end user to be aware that there are multiple modes and to be having to write some checks/guardrails their side about the mode and generally have awareness of how they handle each mode.

@KKonstantinov KKonstantinov self-assigned this Nov 16, 2025
@KKonstantinov
Copy link
Contributor

KKonstantinov commented Nov 16, 2025

Thank you for your work on this!

Have left one comment about .preprocess simplification.

LGTM!

@nbarbettini
Copy link
Contributor Author

nbarbettini commented Nov 17, 2025

@KKonstantinov Done, thanks for your expert guidance! Looks like I need a review from modelcontextprotocol/typescript-sdk-auth in order to merge.

Copy link
Contributor

@felixweinberger felixweinberger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this SEP & accompanying implementation!

Mostly nits on the readme, but one higher level piece of feedback I'd like to discuss.

It looks like on the server-side we implement 2 different methods and types to support this, one for form, one for url.

On the client on the other hand we rely on the mode param to discriminate between the two with a single handler.

This asymmetry seems less than ideal - did we have to do it this way? Was it not possible to discriminate between the modes on the server with the mode field for some reason?

@felixweinberger
Copy link
Contributor

Thank you for this SEP & accompanying implementation!

Mostly nits on the readme, but one higher level piece of feedback I'd like to discuss.

It looks like on the server-side we implement 2 different methods and types to support this, one for form, one for url.

On the client on the other hand we rely on the mode param to discriminate between the two with a single handler.

This asymmetry seems less than ideal - did we have to do it this way? Was it not possible to discriminate between the modes on the server with the mode field for some reason?

Having chatted with @KKonstantinov I understand the main reason for this choice was to avoid a backwards incompatible change right? Changing the type signature of elicitInput would cause a breaking change.

Would it be possible to resolve this via an overload instead?

@nbarbettini
Copy link
Contributor Author

nbarbettini commented Nov 17, 2025

@felixweinberger @cliffhall Thanks for your comments! I've restored a single elicitInput() method that has some overloads (and fixed the readme typos).

Existing code that calls elicitInput() without a mode parameter will still work (backwards compatibility confirmed via a new test). New code should use mode: 'form' or mode: 'url' and the examples have been updated to reflect this.

Comment on lines 347 to 356
* Creates an elicitation request for the given parameters.
* @param params The parameters for the form elicitation request (legacy signature without mode).
* @param options Optional request options.
* @returns The result of the elicitation request.
*/
async elicitUrl(params: Omit<ElicitRequestURLParams, 'mode'>, options?: RequestOptions): Promise<ElicitResult> {
const mode = 'url';
if (!this._clientCapabilities?.elicitation?.[mode]) {
throw new Error(`Client does not support ${mode} elicitation.`);
async elicitInput(params: LegacyElicitRequestFormParams, options?: RequestOptions): Promise<ElicitResult>;
async elicitInput(
params: LegacyElicitRequestFormParams | ElicitRequestFormParams | ElicitRequestURLParams,
options?: RequestOptions
): Promise<ElicitResult> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think we could do something like this to mark specifically the Legacy version as @deprecated. But we can do that in a follow-up as well.

  ... other elicitation implementations

  /**
   * Creates an elicitation request for the given parameters.
   * @deprecated Use the overload with explicit `mode: 'form'` instead.
   * @param params The parameters for the form elicitation request (legacy signature without mode).
   */
  async elicitInput(params: LegacyElicitRequestFormParams, options?: RequestOptions): Promise<ElicitResult>;

  // Implementation signature (not visible to callers)
  async elicitInput(
      params: LegacyElicitRequestFormParams | ElicitRequestFormParams | ElicitRequestURLParams,
      options?: RequestOptions
  ): Promise<ElicitResult> {
      // ...
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I'm happy to do a quick follow-up after this. I can also capture what ends up landing in modelcontextprotocol/modelcontextprotocol#1825

@felixweinberger
Copy link
Contributor

felixweinberger commented Nov 17, 2025

@felixweinberger @cliffhall Thanks for your comments! I've restored a single elicitInput() method that has some overloads (and fixed the readme typos).

Existing code that calls elicitInput() without a mode parameter will still work (backwards compatibility confirmed via a new test). New code should use mode: 'form' or mode: 'url' and the examples have been updated to reflect this.

Thanks @nbarbettini for the quick turnaround!

Requesting review from @pcarleton as auth code owner as well.

@cliffhall
Copy link
Member

@nbarbettini heads-up: you'll have to remove the @deprecated tag. To get SEP-1330 passed, we had to remove all references to the word "deprecated" including tags because, as @dsp-ant pointed out, we currently have no formal deprecation strategy. We just used the term Legacy everywhere (as you have with LegacyElicitRequestFormParams).

@nbarbettini
Copy link
Contributor Author

nbarbettini commented Nov 17, 2025

@cliffhall I don't think I am using @deprecated anywhere. I was in an earlier commit but it should be gone now.

Edit: Oh were you referring to the follow-up suggestion from @felixweinberger? If I am understanding right, we should not add new @deprecated tags to the codebase going forward, is that right?

@felixweinberger
Copy link
Contributor

@nbarbettini heads-up: you'll have to remove the @deprecated tag. To get SEP-1330 passed, we had to remove all references to the word "deprecated" including tags because, as @dsp-ant pointed out, we currently have no formal deprecation strategy. We just used the term Legacy everywhere (as you have with LegacyElicitRequestFormParams).

Hey @cliffhall thanks for pointing that out. I don't think the discussion in that SEP automatically extends to SDKs. Our approach on the protocol right now is to not "deprecate" and just name things as "legacy".

We don't actually call any protocol types "deprecated" here - we're just considering using the @deprecated marker in the TypeScript SDK to indicate to type checkers / IDEs that an API has changed and folks should use the new one when they update. I think that's a separate issue and each SDK can decide on its deprecation strategy independent of the protocol?

For example, there are already multiple uses of @deprecated within the TypeScript SDK: https://github.com/search?q=repo%3Amodelcontextprotocol%2Ftypescript-sdk+%40deprecated&type=code

@felixweinberger felixweinberger merged commit 4debc74 into modelcontextprotocol:main Nov 18, 2025
6 checks passed
@nbarbettini nbarbettini deleted the feat/url-elicitation-final branch November 18, 2025 14:55
@nbarbettini
Copy link
Contributor Author

Thanks all!

@felixweinberger Makes sense about @deprecated to signal SDK-level method stuff. I will work on a quick follow-up PR soon.

@cliffhall
Copy link
Member

@nbarbettini @felixweinberger when I run the server/elicitationUrlExample.ts and client/elicitationUrlExample.ts examples, it does pop open a browser indicating that auth was successful, but the client process still croaks with this error:

Error running MCP client: InvalidRequestError: [
  {
    "code": "invalid_type",
    "expected": "object",
    "received": "undefined",
    "path": [],
    "message": "Required"
  }
]
Screenshot 2025-11-18 at 1 13 28 PM

Are you able to see it successfully run? Can you screenshot that?

@nbarbettini
Copy link
Contributor Author

@cliffhall I might have regressed something after my last bit of refactoring. I will check today!

@nbarbettini
Copy link
Contributor Author

Fix here: #1136

@felixweinberger
Copy link
Contributor

felixweinberger commented Nov 18, 2025

@nbarbettini @felixweinberger when I run the server/elicitationUrlExample.ts and client/elicitationUrlExample.ts examples, it does pop open a browser indicating that auth was successful, but the client process still croaks with this error:

Error running MCP client: InvalidRequestError: [
  {
    "code": "invalid_type",
    "expected": "object",
    "received": "undefined",
    "path": [],
    "message": "Required"
  }
]
Screenshot 2025-11-18 at 1 13 28 PM Are you able to see it successfully run? Can you screenshot that?

Thanks for catching this @cliffhall

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement SEP-1036: URL Mode Elicitation for secure out-of-band interactions

5 participants